package com.aphrodite.io.comm;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Vector;

import javax.microedition.io.Connector;
import javax.microedition.io.SecureConnection;
import javax.microedition.io.StreamConnection;

import com.aphrodite.security.Base64;
import com.aphrodite.xml.XMLReader;
import com.aphrodite.xml.XMLWriter;

//not tested yet
/**
 * XMPP connection API
 * <p>
 * <b>Making an XMPP connection </b><br/> <code>
 * XMPPConnection conn=new XMPPConnection("user@google.com","pass1234","talk.google.com","5223");<br/>
 * conn.addListener(&lt;==Implemented XMPPListener==&gt;);<br/>
 * conn.connect();<br/>
 * ...<br/>
 * ...<br/>
 * In the XMPPListener implementation<br/>
 * public void onAuthPass(final String resource) {<br/>
 * conn.getRoster();
 * <br/>
 * }<br/>
 * 
 *           </code>
 * </p>
 * 
 * @author kentelt
 * 
 */
public class XMPPConnection extends CommConnection {

	final static boolean DEBUG = true;

	private InputStream is;
	private final Vector listeners = new Vector();
	private OutputStream os;
	private final int priority;

	private XMLReader reader;
	private String resource;
	private final boolean use_ssl;
	private final String username, password, jid;

	private XMLWriter writer;

	/**
	 * If you create this object all variables will be saved and the method
	 * {@link #run()} is started to log in on jabber server and listen to parse
	 * incomming xml stanzas. Use {@link #addListener(XMPPListener xl)} to
	 * listen to events of this object.
	 * 
	 * 
	 * @param jid
	 *            the username of the jabber account
	 * @param password
	 *            the passwort of the jabber account
	 * @param resource
	 *            a unique identifier of the used resource, for e.g. "mobile"
	 * @param priority
	 *            the priority of the jabber session, defines on which resource
	 *            the messages arrive
	 * @param server
	 *            the hostname/ip of the jabber server
	 * @param port
	 *            the port number of the jabber server
	 * @param use_ssl
	 *            Use secured connection
	 */
	public XMPPConnection(final String jid, final String password, final String resource, final int priority,
			final String server, final String port, final boolean use_ssl) {
		int i = jid.indexOf('@');
		String host = jid.substring(i + 1);
		this.serverPort = port;
		this.username = jid.substring(0, i);
		this.password = password;
		this.resource = resource;
		this.priority = priority;
		this.jid = jid;
		if (server == null)
			this.serverAddr = host;
		else
			this.serverAddr = server;
		this.use_ssl = use_ssl;
		// this.start();
	}

	/**
	 * Makes a Simple XMPP (non secure) connection.
	 * 
	 * @param jid
	 * @param password
	 * @param server
	 * @param port
	 */
	public XMPPConnection(final String jid, final String password, final String server, final String port) {
		this(jid, password, "Mobile", 10, server, port, false);
	}

	/**
	 * Add a {@link XMPPListener} to listen for events.
	 * 
	 * @param xl
	 *            a XmppListener object
	 */
	public void addListener(final XMPPListener xl) {
		if (!listeners.contains(xl))
			listeners.addElement(xl);
	}

	private void connectionFailed(final String msg) {

		try {
			this.writer.close();
			this.reader.close();
		} catch (IOException e1) {
			System.out.println("Could not close XMPP connection" + e1);
		}
		isConnected = false;
		for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
			XMPPListener xl = (XMPPListener) e.nextElement();
			xl.onConnFailed(msg);
		}
	}

	public void disconnect() {
		try {
			this.writer.endTag();
			this.writer.flush();
			this.writer.close();
			isConnected = false;
		} catch (final IOException e) {
			this.connectionFailed(e.toString());
		}
	}

	/**
	 * Sends a roster query.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	public void getRoster() throws IOException {
		this.writer.startTag("iq");
		this.writer.addAttribute("id", "roster");
		this.writer.addAttribute("type", "get");
		this.writer.startTag("query");
		this.writer.addAttribute("xmlns", "jabber:iq:roster");
		this.writer.endTag(); // query
		this.writer.endTag(); // iq
		this.writer.flush();
	}

	/**
	 * Opens the connection with a stream-tag, queries authentication type and
	 * sends authentication data, which is username, password and resource.
	 * <p>
	 * Login is automatically performed when {@link CommConnection#connect()} is
	 * called or Thread.start() is called
	 * </p>
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	public void login() throws IOException {
		if (!use_ssl) {
			// start stream
			this.writer.startTag("stream:stream");
			this.writer.addAttribute("to", serverAddr);
			this.writer.addAttribute("xmlns", "jabber:client");
			this.writer.addAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
			this.writer.flush();
			// log in
			this.writer.startTag("iq");
			this.writer.addAttribute("type", "set");
			this.writer.addAttribute("id", "auth");
			this.writer.startTag("query");
			this.writer.addAttribute("xmlns", "jabber:iq:auth");

			this.writer.startTag("username");
			this.writer.text(this.username);
			this.writer.endTag();
			this.writer.startTag("password");
			this.writer.text(this.password);
			this.writer.endTag();
			this.writer.startTag("resource");
			this.writer.text(this.resource);
			this.writer.endTag();

			this.writer.endTag(); // query
			this.writer.endTag(); // iq
			this.writer.flush();
		} else {
			String msg = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='"
					+ this.serverAddr + "' version='1.0'>";
			os.write(msg.getBytes());
			os.flush();
			do {
				reader.next();
			} while ((reader.getType() != XMLReader.END_TAG) || (!reader.getName().equals("stream:features")));

			System.out.println("SASL phase1");
			/*
			 * for (Enumeration enu = listeners.elements();
			 * enu.hasMoreElements();) { XmppListener xl = (XmppListener)
			 * enu.nextElement(); xl.onDebug("SASL phase 1"); }
			 */

			// int ghost = is.available();
			// is.skip(ghost);
			msg = "<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>";
			byte[] auth_msg = (username + "@" + serverAddr + "\0" + username + "\0" + password).getBytes();
			msg = msg + Base64.encode(auth_msg) + "</auth>";
			os.write(msg.getBytes());
			os.flush();
			reader.next();
			if (reader.getName().equals("success")) {
				while (true) {
					if ((reader.getType() == XMLReader.END_TAG) && reader.getName().equals("success"))
						break;
					reader.next();
				}
			} else {
				for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
					XMPPListener xl = (XMPPListener) e.nextElement();
					xl.onAuthFail(reader.getName() + ", failed authentication");
				}
				return;
			}
			System.out.println("SASL phase2");
			/*
			 * for (Enumeration enu = listeners.elements();
			 * enu.hasMoreElements();) { XmppListener xl = (XmppListener)
			 * enu.nextElement(); xl.onDebug("SASL phase 2"); }
			 */
			msg = "<stream:stream xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' to='"
					+ this.serverAddr + "' version='1.0'>";
			os.write(msg.getBytes());
			os.flush();
			reader.next();
			while (true) {
				if ((reader.getType() == XMLReader.END_TAG) && reader.getName().equals("stream:features"))
					break;
				reader.next();
			}
			System.out.println("SASL done");
			/*
			 * for (Enumeration enu = listeners.elements();
			 * enu.hasMoreElements();) { XmppListener xl = (XmppListener)
			 * enu.nextElement(); xl.onDebug("SASL done"); }
			 */
			if (resource == null)
				msg = "<iq type='set' id='res_binding'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>";
			else
				msg = "<iq type='set' id='res_binding'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>"
						+ resource + "</resource></bind></iq>";
			os.write(msg.getBytes());
			os.flush();
		}
	}

	/**
	 * Closes the stream-tag and the {@link XMLWriter}.
	 */
	public void logoff() {
		disconnect();
	}

	/**
	 * The main parse methode is parsing all types of XML stanzas
	 * <code>message</code>, <code>presence</code> and <code>iq</code>. Although
	 * ignores any other type of xml.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private void parse() throws IOException {
		if (DEBUG)
			System.out.println("*debug* parsing");
		if (!use_ssl)
			this.reader.next(); // start tag
		while (this.reader.next() == XMLReader.START_TAG) {
			final String tmp = this.reader.getName();
			if (tmp.equals("message")) {
				this.parseMessage();
			} else if (tmp.equals("presence")) {
				this.parsePresence();
			} else if (tmp.equals("iq")) {
				this.parseIq();
			} else {
				this.parseIgnore();
			}
		}
		// java.lang.System.out.println("leave parse() " + reader.getName());
		this.reader.close();
	}

	/**
	 * This method doesn't parse tags it only let the reader go through unknown
	 * tags.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private void parseIgnore() throws IOException {
		int x;
		while ((x = this.reader.next()) != XMLReader.END_TAG) {
			if (x == XMLReader.START_TAG) {
				this.parseIgnore();
			}
		}
	}

	/**
	 * This method parses all info/query stanzas, including authentication
	 * mechanism and roster. It also answers version queries.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private void parseIq() throws IOException {
		if (DEBUG)
			System.out.println("*debug* paeseIq");
		String type = this.reader.getAttribute("type");
		final String id = this.reader.getAttribute("id");
		final String from = this.reader.getAttribute("from");
		if (type.equals("error")) {
			while (this.reader.next() == XMLReader.START_TAG) {
				// String name = reader.getName();
				if (this.reader.getName().equals("error")) {
					final String code = this.reader.getAttribute("code");
					for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
						XMPPListener xl = (XMPPListener) e.nextElement();
						xl.onAuthFail(code + ": " + this.parseText());
					}
				} else {
					this.parseText();
				}
			}
		} else if (type.equals("result") && (id != null) && id.equals("res_binding")) {
			// authorized
			while (true) {
				reader.next();
				String tagname = reader.getName();
				if (tagname != null) {
					if ((reader.getType() == XMLReader.START_TAG) && tagname.equals("jid")) {
						reader.next();
						String rsp_jid = reader.getText();
						int i = rsp_jid.indexOf('/');
						this.resource = rsp_jid.substring(i + 1);
						// java.lang.System.out.println(this.resource);
					} else if (tagname.equals("iq"))
						break;
				}
			}
			for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
				XMPPListener xl = (XMPPListener) e.nextElement();
				xl.onAuthPass(this.resource);
			}
			this.sendPresence(null, null, null, null, this.priority);
		} else {
			// java.lang.System.out.println("contacts list");
			while (this.reader.next() == XMLReader.START_TAG) {
				if (this.reader.getName().equals("query")) {
					if (this.reader.getAttribute("xmlns").equals("jabber:iq:roster")) {
						while (this.reader.next() == XMLReader.START_TAG) {
							if (this.reader.getName().equals("item")) {
								type = this.reader.getAttribute("type");
								String jid = reader.getAttribute("jid");
								String name = reader.getAttribute("name");
								String subscription = reader.getAttribute("subscription");
								// newjid = (jid.indexOf('/') == -1) ? jid :
								// jid.substring(0, jid.indexOf('/'));
								boolean check = true;
								// yctai
								/*
								 * for (Enumeration e = listeners.elements();
								 * e.hasMoreElements();) { XmppListener xl =
								 * (XmppListener) e.nextElement();
								 * xl.onContactRemoveEvent(newjid); }
								 */
								while (this.reader.next() == XMLReader.START_TAG) {
									if (this.reader.getName().equals("group")) {
										for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
											XMPPListener xl = (XMPPListener) e.nextElement();
											xl.onContactEvent(jid, name, this.parseText(), subscription);
										}
										check = false;
									} else {
										this.parseIgnore();
									}
								}
								// if (check && !subscription.equals("remove"))
								if (check) {
									for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
										XMPPListener xl = (XMPPListener) e.nextElement();
										xl.onContactEvent(jid, name, "", subscription);
									}
								}
							} else {
								this.parseIgnore();
							}
						}
						for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
							XMPPListener xl = (XMPPListener) e.nextElement();
							xl.onContactOverEvent();
						}
					} else if (this.reader.getAttribute("xmlns").equals("jabber:iq:version")) {
						while (this.reader.next() == XMLReader.START_TAG) {
							this.parseIgnore();
						}
						// reader.next();
						// send version
						this.writer.startTag("iq");
						this.writer.addAttribute("type", "result");
						this.writer.addAttribute("id", id);
						this.writer.addAttribute("to", from);
						this.writer.startTag("query");
						this.writer.addAttribute("xmlns", "jabber:iq:version");

						this.writer.startTag("name");
						this.writer.text("jxa");
						this.writer.endTag();
						this.writer.startTag("version");
						writer.text("1.0");
						this.writer.endTag();
						this.writer.startTag("os");
						this.writer.text("J2ME");
						this.writer.endTag();

						this.writer.endTag(); // query
						this.writer.endTag(); // iq
					} else {
						this.parseIgnore();
					}
				} else {
					this.parseIgnore();
				}
			}
		}
	}

	/**
	 * This method parses all incoming messages.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private void parseMessage() throws IOException {
		final String from = this.reader.getAttribute("from"), type = this.reader.getAttribute("type");
		String body = null, subject = null;
		while (this.reader.next() == XMLReader.START_TAG) {
			final String tmp = this.reader.getName();
			if (tmp.equals("body")) {
				body = this.parseText();
			} else if (tmp.equals("subject")) {
				subject = this.parseText();
			} else {
				this.parseIgnore();
			}
		}
		// (from, subject, body);
		for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
			XMPPListener xl = (XMPPListener) e.nextElement();
			xl.onMessageEvent((from.indexOf('/') == -1) ? from : from.substring(0, from.indexOf('/')), body);
		}
	}

	/**
	 * This method parses all presence stanzas, including subscription requests.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private void parsePresence() throws IOException {
		final String from = this.reader.getAttribute("from"), type = this.reader.getAttribute("type");
		String status = "", show = "";
		// int priority=-1;
		while (this.reader.next() == XMLReader.START_TAG) {
			final String tmp = this.reader.getName();
			if (tmp.equals("status")) {
				status = this.parseText();
			} else if (tmp.equals("show")) {
				show = this.parseText();
				// else if(tmp.equals("priority"))
				// priority = Integer.parseInt(parseText());
			} else {
				this.parseIgnore();
			}
		}

		if (DEBUG)
			System.out.println("*debug* from,type,status,show:" + from + "," + type + "," + status + "," + show);

		// if ((type != null) && (type.equals("unavailable") ||
		// type.equals("unsubscribed") || type.equals("error"))) {
		if (type == null) {
			for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
				XMPPListener xl = (XMPPListener) e.nextElement();
				xl.onStatusEvent(from, show, status);
			}
		} else {
			if (type.equals("unsubscribed") || type.equals("error")) {
				for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
					XMPPListener xl = (XMPPListener) e.nextElement();
					xl.onUnsubscribeEvent(from);
				}
			} else if (type.equals("subscribe")) {
				for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
					XMPPListener xl = (XMPPListener) e.nextElement();
					xl.onSubscribeEvent(from);
				}
			} else if (type.equals("unavailable")) {
				// final String jid = (from.indexOf('/') == -1) ? from :
				// from.substring(0, from.indexOf('/'));
				for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
					XMPPListener xl = (XMPPListener) e.nextElement();
					// xl.onStatusEvent(jid, show, status);
					xl.onStatusEvent(from, "na", status);
				}
			}
		}
	}

	/**
	 * This method parses all text inside of xml start and end tags.
	 * 
	 * @throws java.io.IOException
	 *             is thrown if {@link XMLReader} or {@link XMLWriter} throw an
	 *             IOException.
	 */
	private String parseText() throws IOException {
		final String endTagName = this.reader.getName();
		final StringBuffer str = new StringBuffer("");
		int t = this.reader.next(); // omit start tag
		while (!endTagName.equals(this.reader.getName())) {
			if (t == XMLReader.TEXT) {
				str.append(this.reader.getText());
			}
			t = this.reader.next();
		}
		return str.toString();
	}

	public void removeAllListeners() {
		listeners.removeAllElements();
	}

	/**
	 * Remove a {@link XMPPListener} from this class.
	 * 
	 * @param xl
	 *            a XmppListener object
	 */
	public void removeListener(final XMPPListener xl) {
		listeners.removeElement(xl);
	}

	/**
	 * The <code>run</code> method is called when {@link XMPPConnection} object
	 * is created. It sets up the reader and writer, calls {@link #login()}
	 * methode and listens on the reader to parse incomming xml stanzas.
	 */
	public void run() {
		try {
			if (!use_ssl) {
				final StreamConnection connection = (StreamConnection) Connector.open("socket://" + this.serverAddr
						+ ":" + this.serverPort);
				this.reader = new XMLReader(connection.openInputStream());
				this.writer = new XMLWriter(connection.openOutputStream());
				isConnected = true;
			} else {
				final SecureConnection sc = (SecureConnection) Connector.open("ssl://" + this.serverAddr + ":"
						+ this.serverPort, Connector.READ_WRITE);
				// sc.setSocketOption(SocketConnection.DELAY, 1);
				// sc.setSocketOption(SocketConnection.LINGER, 0);
				is = sc.openInputStream();
				os = sc.openOutputStream();
				this.reader = new XMLReader(is);
				this.writer = new XMLWriter(os);
				isConnected = true;
			}
		} catch (final IOException e) {
			this.connectionFailed(e.toString());
			isConnected = false;
		}
		/*
		 * for (Enumeration enu = listeners.elements(); enu.hasMoreElements();)
		 * { XmppListener xl = (XmppListener) enu.nextElement();
		 * xl.onDebug("connected"); }
		 */

		// connected
		if (isConnected) {
			try {
				this.login();
				this.parse();
				// java.lang.System.out.println("done");
			} catch (final IOException e) {
				// e.printStackTrace();
				/*
				 * for (Enumeration enu = listeners.elements();
				 * enu.hasMoreElements();) { XmppListener xl = (XmppListener)
				 * enu.nextElement(); xl.onDebug(e.getMessage()); }
				 */
				/*
				 * try { this.writer.close(); this.reader.close(); } catch
				 * (final IOException io) { io.printStackTrace(); }
				 */
				// hier entsteht der connection failed bug (Network Down)
				this.connectionFailed(e.toString());
			}
		}
	}

	/**
	 * Save a contact to roster. This means, a message is send to jabber server
	 * (which hosts your roster) to update the roster.
	 * 
	 * @param jid
	 *            the jid of the contact
	 * @param name
	 *            the nickname of the contact
	 * @param group
	 *            the group of the contact
	 * @param subscription
	 *            the subscription of the contact
	 */
	public void saveContact(final String jid, final String name, final Enumeration group, final String subscription) {
		try {
			this.writer.startTag("iq");
			this.writer.addAttribute("type", "set");
			this.writer.startTag("query");
			this.writer.addAttribute("xmlns", "jabber:iq:roster");
			this.writer.startTag("item");
			this.writer.addAttribute("jid", jid);
			if (name != null) {
				this.writer.addAttribute("name", name);
			}
			if (subscription != null) {
				this.writer.addAttribute("subscription", subscription);
			}
			if (group != null) {
				while (group.hasMoreElements()) {
					this.writer.startTag("group");
					this.writer.text((String) group.nextElement());
					this.writer.endTag(); // group
				}
			}
			this.writer.endTag(); // item
			this.writer.endTag(); // query
			this.writer.endTag(); // iq
			this.writer.flush();
		} catch (final Exception e) {
			// e.printStackTrace();
			this.connectionFailed(e.toString());
		}
	}

	/**
	 * Sends a message text to a known jid.
	 * 
	 * @param to
	 *            the JID of the recipient
	 * @param msg
	 *            the message itself
	 */
	public void sendMessage(final String to, final String msg) {
		try {
			this.writer.startTag("message");
			this.writer.addAttribute("type", "chat");
			this.writer.addAttribute("to", to);
			this.writer.addAttribute("from", jid);
			this.writer.startTag("body");
			this.writer.text(msg);
			this.writer.endTag();// end body tag
			this.writer.endTag();// end message tag
			this.writer.flush();
		} catch (final Exception e) {
			this.connectionFailed(e.toString());
		}
	}

	/**
	 * Sends a presence stanza to a jid. This method can do various task but
	 * it's private, please use setStatus to set your status or explicit
	 * subscription methods subscribe, unsubscribe, subscribed and unsubscribed
	 * to change subscriptions.
	 */
	private void sendPresence(final String to, final String type, final String show, final String status,
			final int priority) {
		try {
			this.writer.startTag("presence");
			if (type != null) {
				this.writer.addAttribute("type", type);
			}
			if (to != null) {
				this.writer.addAttribute("to", to);
			}
			if (show != null) {
				this.writer.startTag("show");
				this.writer.text(show);
				this.writer.endTag();
			}
			if (status != null) {
				this.writer.startTag("status");
				this.writer.text(status);
				this.writer.endTag();
			}
			if (priority != 0) {
				this.writer.startTag("priority");
				this.writer.text(Integer.toString(priority));
				this.writer.endTag();
			}
			this.writer.endTag(); // presence
			this.writer.flush();
		} catch (final Exception e) {
			// e.printStackTrace();
			this.connectionFailed(e.toString());
		}
	}

	/**
	 * Sets your Jabber Status.
	 * 
	 * @param show
	 *            is one of the following: <code>null</code>, chat, away, dnd,
	 *            xa, invisible
	 * @param status
	 *            an extended text describing the actual status
	 * @param priority
	 *            the priority number (5 should be default)
	 */
	public void setStatus(String show, String status, final int priority) {
		if (show.equals("")) {
			show = null;
		}
		if (status.equals("")) {
			status = null;
		}
		if (show.equals("invisible")) {
			this.sendPresence(null, "invisible", null, null, priority);
		} else {
			this.sendPresence(null, null, show, status, priority);
		}
	}

	/**
	 * Requesting a subscription.
	 * 
	 * @param to
	 *            the jid you want to subscribe
	 */
	public void subscribe(final String to) {
		this.sendPresence(to, "subscribe", null, null, 0);
	}

	/**
	 * Approve a subscription request.
	 * 
	 * @param to
	 *            the jid that sent you a subscription request
	 */
	public void subscribed(final String to) {
		this.sendPresence(to, "subscribed", null, null, 0);
	}

	/**
	 * Remove a subscription.
	 * 
	 * @param to
	 *            the jid you want to remove your subscription
	 */
	public void unsubscribe(final String to) {
		this.sendPresence(to, "unsubscribe", null, null, 0);
	}

	/**
	 * Refuse/Reject a subscription request.
	 * 
	 * @param to
	 *            the jid that sent you a subscription request
	 */
	public void unsubscribed(final String to) {
		this.sendPresence(to, "unsubscribed", null, null, 0);
	}
};
